package com.zzyl.nursing.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson2.JSON;
import com.zzyl.common.constant.Constants;
import com.zzyl.nursing.domain.pojo.AlertData;
import com.zzyl.nursing.domain.pojo.AlertRule;
import com.zzyl.nursing.domain.pojo.DeviceData;
import com.zzyl.nursing.domain.vo.AlertNotifyVo;
import com.zzyl.nursing.mapper.DeviceMapper;
import com.zzyl.nursing.service.IAlertDataService;
import com.zzyl.nursing.service.IAlertHandleService;
import com.zzyl.nursing.service.IAlertRuleService;
import com.zzyl.nursing.ws.WebSocketServer;
import com.zzyl.system.mapper.SysUserRoleMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author robin
 * @date 25.6.3 11:46
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class AlertHandleServiceImpl implements IAlertHandleService {

    private final IAlertRuleService alertRuleService;

    private final StringRedisTemplate redisTemplate;

    private final DeviceMapper deviceMapper;

    private final SysUserRoleMapper userRoleMapper;

    private final IAlertDataService alertDataService;

    private final WebSocketServer webSocketServer;

    /**
     * 一、获取所有设备的所有属性数据
     * 0.当没有任何规则时，不需要校验
     * 1.拿到redis中所有设备数据
     * 2.数据做一下转换得到所有属性数据：List<DeviceData>
     * 3.对所有属性数据进行循环遍历进行数据比对：findAllAlertRule(deviceData)
     */
    @Override
    public void alertFilter() {
//      * 0.当没有任何规则时，不需要校验
        List<AlertRule> alertRules = alertRuleService.lambdaQuery().eq(AlertRule::getStatus, 1).list();
        if (CollUtil.isEmpty(alertRules)) {
            return;
        }
//     * 1.拿到redis中所有设备数据
        List<Object> allDeviceDatasJsonList = redisTemplate.opsForHash().values(Constants.IOT_LAST_DEVICE_DATA_LIST);
        if (CollUtil.isEmpty(allDeviceDatasJsonList)) {
            return;
        }
//     * 2.数据做一下转换得到所有属性数据：List<DeviceData>
        ArrayList<DeviceData> deviceDataList = new ArrayList<>();
        allDeviceDatasJsonList.forEach(deviceDataJson -> {
            deviceDataList.addAll(JSONUtil.toList(deviceDataJson.toString(), DeviceData.class));
        });
//     * 3.对所有属性数据进行循环遍历进行数据比对：findAllAlertRule(deviceData)
        deviceDataList.forEach(this::findAllAlertRule);
    }

    /**
     * 二、获取规则表中所有规则
     * 1.判断当前传递过来的属性数据是否在一分钟内
     * 2.从表中获取状态为启用所有规则：List<AlertRule>进行判断
     * 3.对所有报警规则进行遍历：拿每一个规则和这里传递的属性数据进行比对compareHandle(alertRule,deviceData)
     */
    private void findAllAlertRule(DeviceData deviceData) {
//     * 1.判断当前传递过来的属性数据是否在一分钟内
        LocalDateTime alarmTime = deviceData.getAlarmTime();
        LocalDateTime localTime = LocalDateTime.now();
        long between = LocalDateTimeUtil.between(alarmTime, localTime, ChronoUnit.SECONDS);
//        if (between > 60) {
//            return;
//        }
//     * 2.从表中获取状态为启用所有规则：List<AlertRule>进行判断
        List<AlertRule> currentRuleList = alertRuleService.<AlertRule>lambdaQuery()
                .eq(AlertRule::getStatus, 1)
                .eq(AlertRule::getProductKey,deviceData.getProductKey())
                .eq(AlertRule::getIotId,deviceData.getIotId())
                .eq(AlertRule::getFunctionId,deviceData.getFunctionId())
                .list();

        List<AlertRule> allRuleList = alertRuleService.<AlertRule>lambdaQuery()
                .eq(AlertRule::getStatus, 1)
                .eq(AlertRule::getProductKey,deviceData.getProductKey())
                .eq(AlertRule::getIotId,-1)
                .eq(AlertRule::getFunctionId,deviceData.getFunctionId())
                .list();
        // 合
        CollUtil.addAll(allRuleList,currentRuleList);
//     * 3.对所有报警规则进行遍历：拿每一个规则和这里传递的属性数据进行比对compareHandle(alertRule,deviceData)
        allRuleList.forEach(alertRule -> compareHandle(alertRule, deviceData));
    }

    /**
     * 三、拿每一个规则和传递过来的属性数据进行比对
     * 1.【判断】判断：属性数据中存在alarmTime是否在规则中定义有效时间
     * 2.【检查异常】比对：属性数据dataValue和规则中阈值以及比较符进行比对；
     * 3.【redis中沉默周期】如果是异常数据：从redis中查询是否有报警过（有报警就不需要处理，没有报警需要进行下面其他操作）
     * 4.【redis中计数】如果是异常数据且没有报警过：从redis中查询异常数据次数和规则中定义持续周期进行比较看是否构成报警
     * 5.【找通知人】一旦确定报警需要找到报警通知人员并批量插入报警数据  saveBatchAlertData(deviceData,alertRule)
     * 6.【短信或者站内消息通知】websocket进行通知对应报警消息
     */
    private void compareHandle(AlertRule alertRule, DeviceData deviceData) {
        log.info("开始比对dododoodo");
//        1.【判断】判断：属性数据中存在alarmTime是否在规则中定义有效时间
        String alertEffectivePeriod = alertRule.getAlertEffectivePeriod();
        String[] seDate = alertEffectivePeriod.split("~");
        LocalTime start = LocalTime.parse(seDate[0], DateTimeFormatter.ofPattern("HH:mm:ss"));
        LocalTime end = LocalTime.parse(seDate[1], DateTimeFormatter.ofPattern("HH:mm:ss"));
        LocalTime alarmTime = deviceData.getAlarmTime().toLocalTime();
        if (alarmTime.isBefore(start)||alarmTime.isAfter(end)) {
            return;
        }
//        2.【检查异常】比对：属性数据dataValue和规则中阈值以及比较符进行比对；
        Float functionIdDataValue =Float.parseFloat(deviceData.getDataValue()) ;
        String operator = alertRule.getOperator();
        Float functionIdRuleValue = alertRule.getValue();
        int compare = Float.compare(functionIdDataValue, functionIdRuleValue);
        boolean isAbnormal = compare >= 0 && operator.equals(">=") || compare < 0 && operator.equals("<");
        // 如果不是异常数据，则不需要处理
        String abnormalDeviceDataCountKey = Constants.IOT_COUNT_ALERT + deviceData.getIotId() + ":" + alertRule.getId();
        if(isAbnormal){
            // 删除redis中的异常数据计数
            redisTemplate.delete(abnormalDeviceDataCountKey);
            return;
        }
//        3.【redis中沉默周期】如果是异常数据：从redis中查询是否有报警过（有报警就不需要处理，没有报警需要进行下面其他操作）
        String abnormalSilentAlertKey = Constants.IOT_SILENT_ALERT+ deviceData.getIotId() + ":" + alertRule.getId();
        if(BooleanUtil.isTrue(redisTemplate.hasKey(abnormalSilentAlertKey))){
            log.info("该设备已经报警过，不需要重复报警，{}分钟后会重启检测报警",alertRule.getAlertSilentPeriod());
            return;
        }

//        4.【redis中计数】如果是异常数据且没有报警过：从redis中查询异常数据次数和规则中定义持续周期进行比较看是否构成报警
        String abnormalCounts = redisTemplate.opsForValue().get(abnormalDeviceDataCountKey);
        int abnormalDeviceAlertCounts = StrUtil.isNotBlank(abnormalCounts) ? Integer.parseInt(abnormalCounts) : 0;
        Integer duration = alertRule.getDuration();
        if(abnormalDeviceAlertCounts+1<duration){
            // 此时次数没有达到，redis次数+1
            redisTemplate.opsForValue().increment(abnormalDeviceDataCountKey);
            return;
        }
        log.info("异常数据连续{}次，需要报警",duration);
        // 触发报警后，删除redis中的计数，并设置沉默周期
        redisTemplate.delete(abnormalDeviceDataCountKey);
        redisTemplate.opsForValue().set(abnormalSilentAlertKey,"true",alertRule.getAlertSilentPeriod(), TimeUnit.MINUTES);
//        5.【找通知人】一旦确定报警需要找到报警通知人员并批量插入报警数据  saveBatchAlertData(deviceData,alertRule)
        List<Long> userIds = saveBatchAlertData(deviceData, alertRule);
//        6.【短信或者站内消息通知】websocket进行通知对应报警消息
        sendMsgNotify(alertRule,deviceData,userIds);
    }

    /**
     * 四、找报警通知人员批量保存报警数据
     * 1.要找到通知人员
     * 2.有多少个通知人员就构建多个报警数据然后批量保存到报警数据表中
     */
    private List<Long> saveBatchAlertData(DeviceData deviceData, AlertRule alertRule) {
//        1.要找到通知人员
        Integer physicalLocationType = deviceData.getPhysicalLocationType();
        List<Long> notifyIds = new ArrayList<>();
        switch (physicalLocationType){
            // 随身设备，设备bindingLocation->elder->nursing_elder
            case -1:
                notifyIds = deviceMapper.selectNursingIdsByIotIdWithElder(deviceData.getIotId());
                break;
                // 房间设备：根据角色找用户（维修工）
            case 1:
                notifyIds = userRoleMapper.selectUserIdByRoleName("维修工");
                break;
                // 床上设备，床-》老人-》护士
            case 2:
                notifyIds = deviceMapper.selectNursingIdsByIotIdWithBed(deviceData.getIotId());
                break;
        }
        // 通知到管理员
        notifyIds.addAll(userRoleMapper.selectUserIdByRoleName("超级管理员"));
//        2.有多少个通知人员就构建多个报警数据然后批量保存到报警数据表中
        List<AlertData> alertDataList = new ArrayList<>();
        notifyIds.forEach(notifyId->{
            AlertData alertData = BeanUtil.toBean(deviceData, AlertData.class, new CopyOptions().setIgnoreProperties("id", "createBy", "createTime","remark"));
            alertData.setAlertRuleId(alertRule.getId());
            String alertReason = StrUtil.format("报警设备名称{},报警原因：{}{}{},报警持续周期{}", deviceData.getDeviceName(), alertRule.getFunctionId(), alertRule.getOperator(), alertRule.getValue(), alertRule.getDuration());
            alertData.setAlertReason(alertReason);
            alertData.setType(alertRule.getAlertDataType());
            alertData.setStatus(0);
            alertData.setUserId(notifyId);
            alertDataList.add(alertData);
        });
        alertDataService.saveBatch(alertDataList);
        return notifyIds;
    }

    /**
     * 五、短信或者站内通知
     * 1.构建通知消息模板
     * 2.通过websocket来实现站内通知
     */
    private void sendMsgNotify(AlertRule alertRule,DeviceData deviceData,List<Long> userIdList) {
        AlertNotifyVo alertNotifyVo=new AlertNotifyVo();
        BeanUtil.copyProperties(deviceData,alertNotifyVo);
        alertNotifyVo.setFunctionName(alertRule.getFunctionId());
        alertNotifyVo.setAlertDataType(alertRule.getAlertDataType());
        alertNotifyVo.setVoiceNotifyStatus(0);
        alertNotifyVo.setNotifyType(1);
        webSocketServer.sendMessageToConsumer(alertNotifyVo,userIdList);
    }
}
